fix: preserve authorizer config for overlapping API routes - #9166
fix: preserve authorizer config for overlapping API routes#9166Zahoor-ishfaq wants to merge 9 commits into
Conversation
| authorizer_name=route.authorizer_name, | ||
| authorizer_object=route.authorizer_object, | ||
| use_default_authorizer=route.use_default_authorizer, | ||
| cors=route.cors, |
There was a problem hiding this comment.
[BUG] Route-level CORS is no longer propagated across the routes in a group. The removed code deliberately merged it:
# Prefer route-specific CORS over None
cors = route.cors if route.cors is not None else (config.cors if config else None)The new code only carries cors forward inside the if matching_route: branch, i.e. only between routes that share an authorizer config. When a group now splits because the authorizers differ, the non-OPTIONS route is constructed with cors=route.cors, which is None for every non-OPTIONS method — cfn_api_provider.py only ever attaches route-level CORS to OPTIONS methods:
cors=cors if method == "OPTIONS" else None,There is no fallback to recover it. In local_apigw_service._request_handler:
cors = route.cors if route.cors is not None else self.api.cors
...
headers.update(cors_headers)self.api.cors is not set in this path either, because cfn_api_provider assigns collector.cors only in the elif cors: branch for non-OPTIONS methods. So for a REST API whose OPTIONS method carries the CORS integration responses while its other methods carry an authorizer, the preflight still succeeds but the actual GET/POST response is returned with no Access-Control-Allow-* headers, and the browser rejects it. Previously the single merged route held cors and all methods got the headers.
Compute the group's effective CORS once and apply it to every resulting route that has none of its own:
group_cors = next((route.cors for route in route_group if route.cors is not None), None)
...
for merged_route in merged_routes:
if merged_route.cors is None:
merged_route.cors = group_cors
result.extend(route for route in merged_routes if route.methods)| return ( | ||
| first.authorizer_name == second.authorizer_name | ||
| and first.authorizer_object == second.authorizer_object | ||
| and first.use_default_authorizer == second.use_default_authorizer |
There was a problem hiding this comment.
[GENERAL] Including use_default_authorizer in has_same_authorizer splits routes whose effective authorization is identical.
dedupe_function_routes runs after _link_authorizers() (see get_api), and by that point use_default_authorizer is inert — the only consumers are _link_authorizers itself and the catch-all copy in local_apigw_service._add_catch_all_path. _request_handler dispatches purely on route.authorizer_object. Resolution has already collapsed the flag: a route with use_default_authorizer=True either received the default authorizer's name and object, or had authorizer_name reset to None.
So for a template with no default authorizer:
Events:
Get:
Type: Api
Properties: {Path: /x, Method: get}
Post:
Type: Api
Properties: {Path: /x, Method: post, Auth: {Authorizer: NONE}}both routes end up with authorizer_name=None and authorizer_object=None, but use_default_authorizer is True vs False (sam_api_provider.py:530-537). They are emitted as two separate Route objects for the same function and path where dedupe previously produced one, adding a redundant Flask URL rule and a duplicate entry in the sam local start-api mount listing — for two routes that behave identically.
Compare only the resolved authorizer:
def has_same_authorizer(first: Route, second: Route) -> bool:
return first.authorizer_name == second.authorizer_name and first.authorizer_object == second.authorizer_objectThis still keeps the case in issue #9165 separate — the ANY route resolves to MyAuthorizer while the explicit OPTIONS route resolves to None — and use_default_authorizer is still preserved on the emitted Route, which is the part that was genuinely missing before.
|
|
||
| for existing_route in merged_routes: | ||
| if not has_same_authorizer(existing_route, route): | ||
| existing_route.methods = [method for method in existing_route.methods if method not in methods] |
There was a problem hiding this comment.
[BUG] Stripping the overlapping methods makes the split disjoint here, but get_api() runs the CORS normalization immediately after dedupe (lines 195-196):
routes = self.dedupe_function_routes(self.routes)
routes = self.normalize_cors_methods(routes, self.cors)and normalize_cors_methods appends OPTIONS back onto every route that does not already have it:
def add_options_to_route(route: Route) -> Route:
if "OPTIONS" not in route.methods:
route.methods.append("OPTIONS")
return routeSo whenever the API has CORS configured, the authorizer-protected route regains OPTIONS and both split routes claim <path>:OPTIONS. LocalApigwService.create() keys _dict_of_routes by path:method only (local_apigw_service.py:145-146), so one entry silently overwrites the other and the winner is whichever route comes later in api.routes. That means the "explicit route owns the overlapping method" rule this PR establishes is re-decided by list position: it currently produces the right result only because the broadest route is appended first by the sort on line 273.
Consider making the released methods explicit instead of relying on ordering — e.g. track the methods a route gave up during dedupe and have normalize_cors_methods skip injecting OPTIONS into a route that deliberately released it (or skip injection when another route in the same group already serves OPTIONS). Otherwise the order-dependence the PR removes from dedupe_function_routes reappears one step later in the pipeline.
| ) | ||
|
|
||
| if matching_route: | ||
| matching_route.methods = sorted(set(matching_route.methods + methods)) |
There was a problem hiding this comment.
[GENERAL] When two routes in a group share an authorizer, the merge only updates methods and cors on the surviving route — which is the first one created, i.e. the route with the most methods (usually the ANY route). Every other attribute of the narrower route is discarded, including payload_format_version.
That one has real consequences for HTTP APIs, because a missing payload format version is treated as 2.0 (local_apigw_service.py:460):
if route.event_type == Route.HTTP and route.payload_format_version in [None, "2.0"]:So an ANY route with no PayloadFormatVersion now deterministically shadows a sibling method route (same function/path/operation and same authorizer) that declares "1.0", and that method's Lambda receives a v2 event instead of a v1 event. SamApiProvider.merge_routes already guards against exactly this loss:
if route and route.payload_format_version and config.payload_format_version is None:
config.payload_format_version = route.payload_format_versionMirroring it in the merge branch keeps the behavior consistent:
if matching_route:
matching_route.methods = sorted(set(matching_route.methods + methods))
if matching_route.payload_format_version is None:
matching_route.payload_format_version = route.payload_format_version
if route.cors is not None:
matching_route.cors = route.cors
continueThe same one-sided loss applies to use_default_authorizer, which the PR description says is preserved: it is preserved on the copy (line 302) but on a merge only the first route's value survives, and test_merges_routes_with_same_resolved_authorizer locks that in (the False from the POST route is dropped). That is currently inert because _link_authorizers() has already run, but it is worth a comment in the code so the next reader does not assume the flag is still meaningful.
| ) | ||
| return list(grouped_routes.values()) | ||
|
|
||
| for route_group in grouped_routes.values(): |
There was a problem hiding this comment.
[GENERAL] The split logic here is correct, but for SAM templates it is only reachable when the explicit OPTIONS event is declared after the ANY event, so the order-dependence the PR aims to remove still exists one layer up.
SamApiProvider.merge_routes() runs before get_api() (see sam_api_provider.py:100) and de-dupes by path + method, keeping the last writer per key:
for config in all_configs:
# Normalize the methods before de-duping to allow an ANY method in implicit API to override a regular HTTP
# method on explicit route.
for normalized_method in config.methods:
key = config.path + normalized_method
...
all_routes[key] = config
result = set(all_routes.values()) # Assign to a set() to de-dupeBecause Route.normalize_method already expands ANY into all seven verbs, the ANY route claims the /{proxy+}OPTIONS key too. The explicit OPTIONS route only has that one key, so if it is written first and then overwritten, it is no longer a value in all_routes and is dropped by set(all_routes.values()) — dedupe_function_routes never sees it, and the OPTIONS method keeps the ANY route's authorizer.
Concretely, for the template in #9165:
ANYevent declared first,OPTIONSevent second → both routes survivemerge_routes→ this fix applies.OPTIONSevent declared first,ANYsecond → theOPTIONSroute is discarded → preflight is still authorized.ANYevent implicit (noRestApiId) andOPTIONSexplicit → implicit routes are iterated last by design, so theOPTIONSroute is always discarded.
The new tests all call dedupe_function_routes/get_api directly, so they cannot catch this. Please either give the more specific method precedence in merge_routes (an explicit single-method route should not be clobbered by an expanded ANY route) or add a test that drives the scenario through SamApiProvider.extract_resources with the OPTIONS event declared first, so the end-to-end behavior is pinned.
| route | ||
| and len(route.methods) == 1 | ||
| and set(config.methods) == set(Route.ANY_HTTP_METHODS) | ||
| and route.function_name == config.function_name |
There was a problem hiding this comment.
[BUG] The preservation condition pins function_name, stack_path and event_type to be equal, but not operation_name. That matters because it is the only field in this condition that is also part of the grouping key used later by dedupe_function_routes:
[REDACTED] route.function_name, route.path, route.operation_name or "")When the preserved narrow route and the skipped ANY route have different operation_name values, they land in different groups, so the new method-stripping logic in dedupe_function_routes never runs between them. The ANY route keeps the overlapping method in config.methods and both routes survive.
This is reachable: swagger-derived routes get operation_name from operationId (swagger/parser.py), while SAM Api/HttpApi events leave it None. A function referenced from an explicit DefinitionBody options: entry with an operationId, plus an implicit ANY event on the same path, produces two routes both claiming OPTIONS for that path.
Downstream, LocalApigwService.create() registers a Flask rule per route and fills _dict_of_routes keyed by path:method, so the two entries collide and the authorizer actually applied to OPTIONS depends on route ordering — the order-dependence this PR sets out to remove.
Adding operation_name to the condition keeps the routes in the same dedupe group (or lets the pre-existing clobber happen), so the overlap is always resolved:
if (
route
and len(route.methods) == 1
and set(config.methods) == set(Route.ANY_HTTP_METHODS)
and route.function_name == config.function_name
and route.stack_path == config.stack_path
and route.event_type == config.event_type
and route.operation_name == config.operation_name
):
continue| # expanded ANY route overlaps it. This keeps explicit method intent | ||
| # independent of declaration order while retaining the existing | ||
| # precedence rules between different functions and stacks. | ||
| if ( |
There was a problem hiding this comment.
[GENERAL] The new rule is broader than the OPTIONS scenario in the PR description: it preserves any single-method route of the same function against an expanding ANY route, regardless of the authorizer configuration. That reverses the precedence this function documents and that the loop comment immediately above still asserts:
# Normalize the methods before de-duping to allow an ANY method in implicit API to override a regular HTTP
# method on explicit route.Concretely, for a function with an explicit GET /x event on Api1 plus an implicit ANY /x event, GET previously resolved to the implicit route; it now resolves to the explicit one, so Api1's authorizer (and default authorizer) applies to GET instead of the implicit API's. Nothing about OPTIONS or CORS is involved, and no test in the diff covers it — both new tests assert only the OPTIONS case.
Since dedupe_function_routes now splits by authorizer anyway, the skip only needs to happen when the two routes would not merge. Gating on differing authorizer intent keeps the fix targeted and leaves the documented implicit-over-explicit precedence intact for everything else:
and (
route.authorizer_name != config.authorizer_name
or route.use_default_authorizer != config.use_default_authorizer
)Note this comparison must use the raw authorizer_name/use_default_authorizer fields, since merge_routes runs before _link_authorizers() and authorizer_object is still unset at this point. If the broader behavior is intentional, the stale loop comment and the merge_routes docstring should be updated to describe the new precedence.
| and route.event_type == config.event_type | ||
| and (route.operation_name or "") == (config.operation_name or "") | ||
| and ( | ||
| route.authorizer_name != config.authorizer_name |
There was a problem hiding this comment.
[BUG] The preservation rule triggers on any difference in raw authorizer intent, including the case where the narrow route simply does not declare one. That silently strips an event-level authorizer from ordinary (non-OPTIONS) methods.
Concrete case — an explicit AWS::Serverless::Api whose DefinitionBody declares get /x with no security key, plus a function event ANY /x with Auth: {Authorizer: MyAuthorizer}:
SwaggerParser.get_routes()(samcli/commands/local/lib/swagger/parser.py:360-361) setsauthorizer_name=None, use_default_authorizer=Truewhensecurityis absent — that is "unspecified", not "no auth"._convert_event_route()setsauthorizer_name="MyAuthorizer"for theANYevent.- Both routes land in
explicit_routes(sameRestApiId), samefunction_name,stack_path,event_type, andoperation_name. WithApi1declared before the function inResources, the swagger route is stored first. route.authorizer_name != config.authorizer_nameis then true, socontinuekeeps the swagger route as the owner of theGETkey._link_authorizers()resolves the preserved route viadefault_authorizer. If the API has noDefaultAuthorizer,GET /xends up withauthorizer_name=Noneand_request_handlerinvokes the function with no authorizer — previously theANYroute won and the request was authorized.
So a route that is authorized on AWS becomes unauthenticated under sam local start-api, which is the opposite of the PR's goal and defeats the purpose of testing an authorizer locally. It also contradicts the contract this function still documents ("Implicit API definition wins because that conveys clear intent") and the loop comment two lines above ("to allow an ANY method in implicit API to override a regular HTTP method on explicit route").
Gating on an explicit opt-out rather than any mismatch keeps the OPTIONS/Authorizer: NONE and security: [] scenarios working while leaving unspecified methods on the existing precedence path:
and (
# Only preserve when the narrower route explicitly declares its own
# authorizer intent. A missing swagger "security" key means unspecified,
# not "no authorization", and must not override the ANY route.
(route.authorizer_name is not None or not route.use_default_authorizer)
and (
route.authorizer_name != config.authorizer_name
or route.use_default_authorizer != config.use_default_authorizer
)
)Please also update the merge_routes docstring and the stale loop comment to describe the new conditional precedence, and add a test for "swagger method without security + ANY event with an authorizer" asserting the method stays authorized.
There was a problem hiding this comment.
Code Review Results
Reviewed: 9101836..4e8413c
Files: 4
Comments: 2
Comments on lines outside the diff:
[samcli/lib/providers/sam_api_provider.py:600] [GENERAL] payload_format_version is copied onto the ANY route from a route that is then preserved rather than overridden.
The inheritance runs before the new preservation block, so by the time continue fires, config (the ANY route) has already absorbed route.payload_format_version from the narrow route it no longer overrides:
route = all_routes.get(key)
if route and route.payload_format_version and config.payload_format_version is None:
config.payload_format_version = route.payload_format_version
if (... preservation condition ...):
continueFor an HTTP API where the narrow event declares PayloadFormatVersion: "1.0" and the ANY event declares none, the ANY route silently becomes 1.0. local_apigw_service treats None as 2.0 (route.payload_format_version in [None, "2.0"] at line 460, and the same check at line 766), so the event and response shape changes for the remaining six methods even though the two events are now independent routes.
Move the preservation check ahead of the inheritance:
route = all_routes.get(key)
if (
route
and len(route.methods) == 1
and set(config.methods) == set(Route.ANY_HTTP_METHODS)
# ... rest of the condition
):
continue
if route and route.payload_format_version and config.payload_format_version is None:
config.payload_format_version = route.payload_format_version
all_routes[key] = config| options_claimed = any("OPTIONS" in route.methods for route in route_group) | ||
|
|
||
| for route in route_group: | ||
| if not options_claimed: |
There was a problem hiding this comment.
[BUG] The synthesized OPTIONS owner is chosen by group iteration order, so it can land on an authorizer-protected route even when an unauthenticated sibling exists in the same group.
After the dedupe_function_routes change, a group can legitimately contain several routes with different authorizers. Consider an AWS::Serverless::Api with Cors set and a DefinitionBody where /x get has security: [{MyAuth: []}] and /x post has security: []. The Swagger parser gives both the same backing function, no operationId, and the same path, so they share the dedupe group key and now survive as two separate routes ([GET] with MyAuth, [POST] with none). Neither contains OPTIONS, so options_claimed is False and the first route in the group — Swagger paths declaration order — gets OPTIONS appended.
If that first route is get, preflight is served by a route carrying authorizer_object, and _request_handler invokes the Lambda authorizer for it. Browser preflight requests do not carry credentials, so the preflight fails with 401/403 and the real request is never sent. Before this PR these routes were collapsed into one, so this selection did not exist.
Since keeping preflight out of the authorizer path is the point of the PR, pick the owner deliberately rather than by position:
for route_group in grouped_routes.values():
options_claimed = any("OPTIONS" in route.methods for route in route_group)
if not options_claimed:
# Prefer a route without an authorizer so CORS preflight is not authorized
owner = next(
(route for route in route_group if route.authorizer_object is None),
route_group[0],
)
owner.methods.append("OPTIONS")
result.extend(route_group)| if route and route.payload_format_version and config.payload_format_version is None: | ||
| config.payload_format_version = route.payload_format_version | ||
|
|
||
| # Preserve a single-method route when it explicitly declares different |
There was a problem hiding this comment.
[BUG] The payload_format_version inheritance at lines 600-601 runs before the new preservation block, so when continue fires the config object has already absorbed payload_format_version from a route it no longer overrides.
That inheritance only made sense under the old semantics, where config unconditionally replaced route for this key. Now route survives and keeps its own value, while config — the shared ANY route object registered under the other six method keys — silently picks up the narrow route's version.
Concrete case, both events implicit on the same HTTP API:
Events:
Options:
Type: HttpApi
Properties:
Path: /x
Method: OPTIONS
PayloadFormatVersion: "1.0"
Auth: { Authorizer: NONE }
Any:
Type: HttpApi
Properties:
Path: /x
Method: ANY
Auth: { Authorizer: MyAuth }The OPTIONS route is preserved (as intended), but the ANY route now carries payload_format_version == "1.0" even though the user never set one on it. Since it is the same object behind the GET/POST/... keys, every method on /x is then invoked with a v1.0 event instead of the HTTP API default of 2.0 — local_apigw_service.py:460 selects the event shape with route.payload_format_version in [None, "2.0"].
Moving the inheritance below the preservation check keeps it for the keys where config actually wins:
for normalized_method in config.methods:
key = config.path + normalized_method
route = all_routes.get(key)
# Preserve a single-method route when it explicitly declares different
# raw authorizer intent and both routes can be reconciled downstream.
if (
route
and len(route.methods) == 1
# ... unchanged conditions ...
):
continue
if route and route.payload_format_version and config.payload_format_version is None:
config.payload_format_version = route.payload_format_version
all_routes[key] = config| and len(route.methods) == 1 | ||
| and set(config.methods) == set(Route.ANY_HTTP_METHODS) | ||
| and route.function_name == config.function_name | ||
| and route.stack_path == config.stack_path |
There was a problem hiding this comment.
[GENERAL] The operation_name equality clause means the bug this PR targets is still reproducible whenever the narrow route carries an operationId, and the new test suite locks that gap in rather than flagging it.
A swagger-defined preflight normally has an operationId:
paths:
/x:
options:
operationId: Preflight
security: [] # explicitly unauthenticated
x-amazon-apigateway-integration: {...} # -> SamFunc1plus an ANY /x event on the same function with Auth: {Authorizer: MyAuthorizer}. Here route.operation_name == "Preflight" while config.operation_name is None, so the guard short-circuits, all_routes["/xOPTIONS"] = config executes, and the swagger route is discarded entirely. The preflight then runs under MyAuthorizer — the exact behaviour #9165 reports. test_operation_name_mismatch_does_not_leave_duplicate_options_routes asserts this outcome (assertEqual(options_routes[0].authorizer_name, "MyAuthorizer")), so the case is knowingly excluded but reads as intended behaviour to a future reader.
I understand why the clause is there: dedupe_function_routes groups by "{stack_path}-{function_name}-{path}-{operation_name}", so two preserved routes with different operation_name land in different groups, the method-stripping in dedupe_function_routes never runs across them, and both would keep OPTIONS. local_apigw_service._dict_of_routes is keyed on path:method, so the last registration would silently win — worse than the current partial fix.
Two options that close the gap instead of excluding it:
- Drop
operation_namefrom the grouping key indedupe_function_routesandnormalize_cors_methodsso overlapping routes are reconciled regardless ofoperationId. The merged route already discards the narrower route'soperation_nametoday, so this mainly widens the grouping. - Keep the clause but record the limitation in the
merge_routesdocstring and rename the test to state that a differingoperationIdis an unsupported case, so the assertion is not mistaken for desired behaviour.
Either way the current test name reads as "no duplicates" when what it actually pins down is "authorization is applied to a route that declared security: []".
Scope of what I verified (workspace is the merge-base, so I traced the post-merge logic by hand rather than executing it): the previously raised findings are addressed in this revision — route-level CORS is restored via group_cors, use_default_authorizer is excluded from has_same_authorizer, normalize_cors_methods no longer re-appends OPTIONS onto a route whose sibling already owns it, payload_format_version inheritance moved after the continue, function_name/stack_path/event_type/operation_name are pinned in the preservation guard, and the guard now requires the narrow route to declare explicit authorizer intent. I also traced both event-declaration orders (OPTIONS before ANY and after) and confirmed dedupe_function_routes produces disjoint method sets in each, and checked that multiple Route objects sharing one path register cleanly in Flask/Werkzeug (same bound view_func, method-aware rule matching).
|
|
||
| # Preserve a single-method route when it explicitly declares different | ||
| # raw authorizer intent and both routes can be reconciled downstream. | ||
| if ( |
There was a problem hiding this comment.
[BUG] The preservation rule is asymmetric: it is only consulted when the expanded ANY route is the second route to reach a given path+method key. When the ANY route is processed first, len(route.methods) == 1 is false (it is 7), the block is skipped entirely, and the narrow route unconditionally overwrites the key. That means the explicit-intent guard on line 612 (route.authorizer_name is not None or not route.use_default_authorizer) is bypassed, and authorization for a method still flips based on declaration order.
Trace with two Api events on the same function/path in one explicit AWS::Serverless::Api (so both land in explicit_routes at the same stack depth, preserving template order):
Events:
Any:
Type: Api
Properties: { Path: /x, Method: ANY, RestApiId: Api1, Auth: { Authorizer: MyAuthorizer } }
Get:
Type: Api
Properties: { Path: /x, Method: GET, RestApiId: Api1 } # no Auth- Any first: all 7 keys map to anyRoute. Then Get is processed — the preservation block is skipped because len(anyRoute.methods) == 7, so
all_routes["/xGET"] = getRoute. Both routes survive. dedupe_function_routes then sees authorizer_name="MyAuthorizer" vs None, so can_merge is false, strips GET off the ANY route, and keeps getRoute — GET /x ends up unauthenticated. - Get first:
all_routes["/xGET"] = getRoute, then Any evaluates the preservation block. The explicit-intent guard fails (authorizer_name is None and use_default_authorizer is True), so anyRoute overwrites the key and getRoute is dropped entirely — GET /x ends up authenticated with MyAuthorizer.
Same template semantics, opposite authorization outcome purely from YAML key order. This is the order-dependence the PR sets out to remove, and it is the security-relevant direction (authorization silently dropped). Note test_must_prefer_implicit_any_for_same_function_with_same_authorizer_intent only exercises the narrow-first ordering, so the gap is not covered.
The rule needs to be applied from both directions: when a narrow route overwrites a key currently held by an expanded ANY route of the same function/stack/event type and the narrow route declares no authorizer intent, it should end up with the ANY route's authorizer configuration rather than silently winning with authorizer_name=None. I'd avoid prescribing an exact snippet here since mutating config in place has knock-on effects, but the condition needs a mirrored branch keyed on set(route.methods) == set(Route.ANY_HTTP_METHODS) and len(config.methods) == 1.
| result: List[Route] = [] | ||
|
|
||
| for route_group in grouped_routes.values(): | ||
| options_claimed = any("OPTIONS" in route.methods for route in route_group) |
There was a problem hiding this comment.
[GENERAL] options_claimed short-circuits the new unauthenticated-owner preference whenever the group contains an ANY-derived route, because Route.normalize_method expands ANY to ANY_HTTP_METHODS, which always includes OPTIONS. So the docstring's claim that "synthesized OPTIONS prefers a route without a linked local authorizer" does not hold for the most common CORS shape.
Concretely, an AWS::Serverless::Api with Cors set and a group of ANY /x (authorizer MyAuth) plus POST /x (security: []): dedupe_function_routes strips POST from the ANY route but leaves OPTIONS on it, so options_claimed is True and OPTIONS stays owned by the authorizer-protected route. _request_handler dispatches on route.authorizer_object, so the local preflight is challenged. On AWS the Cors property causes the transform to emit an unauthenticated OPTIONS mock integration that overrides ANY, so preflight succeeds there — a browser-visible local/deployed divergence.
The check that matters is not "does any route in the group list OPTIONS" but "does any route explicitly declare OPTIONS". An OPTIONS entry that only exists because ANY was expanded is not an ownership claim, and when cors is configured it should be re-assignable to an unauthorized sibling the same way an explicitly declared OPTIONS route already takes precedence in dedupe_function_routes. As written, the preference only ever fires for groups made up entirely of single-method routes — which is what the new tests cover.
Description
Fixes #9165.
When an
ANYroute and an explicitOPTIONSroute use different authorizer configurations,dedupe_function_routes()previously merged them into a single route.That caused the route authorizer configuration to become order-dependent and could incorrectly apply or remove authorization from the
OPTIONSroute.This change:
OPTIONSroute to override theOPTIONSmethod fromANYuse_default_authorizerTesting
Added a regression test covering:
ANYroute using a Lambda authorizerOPTIONSroute with authorization disabledRelevant unit tests pass:
git diff --checkpasses